package worker

import (
	"../common"
	"time"
	"fmt"
)

//任务调度
type Scheduler struct {
	jobEventChan     chan *common.JobEvent               //etcd任务事件队列
	jobPlanTable     map[string]*common.JobSchedulerPlan //任务调度计划表,每一个任务的下次执行计划时间都在这里面
	jobExcutingTable map[string]*common.JobExecuteInfo   //任务执行表,正在执行中的任务
	jobResultChan    chan *common.JobExecuteResult       //任务结果队列
}

var (
	G_scheduler *Scheduler
)

//处理任务事件,维护map,保证map里面的任务数据和etcd中的一致
func (scheluder *Scheduler) handleJobEvent(jobEvent *common.JobEvent) {
	var (
		jobSchedulerPlan *common.JobSchedulerPlan
		err              error
		jobExisted       bool

		jobExecuteInfo *common.JobExecuteInfo
		jobExecuting   bool
	)
	switch jobEvent.EventType {

	case common.JOB_EVENT_SAVE: //保存任务事件
		if jobSchedulerPlan, err = common.BuildJobSchedulerPlan(jobEvent.Job); err != nil {
			return
		}
		//将返回的对象保存到内存表中
		scheluder.jobPlanTable[jobEvent.Job.Name] = jobSchedulerPlan

	case common.JOB_EVENT_DELETE: //删除任务事件,判断内存表中有没有,有则直接删除
		if jobSchedulerPlan, jobExisted = scheluder.jobPlanTable[jobEvent.Job.Name]; jobExisted {
			delete(scheluder.jobPlanTable, jobEvent.Job.Name)
		}

	case common.JOB_EVENT_KILL: //强杀任务事件
		//取消command执行
		//(1)判断任务是否在执行中
		if jobExecuteInfo, jobExecuting = scheluder.jobExcutingTable[jobEvent.Job.Name]; jobExecuting {
			jobExecuteInfo.CancelFunc() //触发command杀死shell子进程,任务得到退出
		}

	}
}

//尝试执行任务
func (scheduler *Scheduler) TryStartJob(jobPlan *common.JobSchedulerPlan) {
	var (
		jobExecuteInfo *common.JobExecuteInfo
		jobExecuting   bool
	)
	//调度和执行是两件事情

	//执行的任务可能会运行很久,比如任务执行需要1分钟,但是我们每秒调度一次
	//这样就会一分钟调度60次,但是我们需求是只能执行一次，这次执行完毕才能执行下一次
	//防止并发,如果任务在内存任务状态表中,则说明他还在执行,则这个任务就不执行了

	//如果任务正在执行,跳过本次任务
	if jobExecuteInfo, jobExecuting = scheduler.jobExcutingTable[jobPlan.Job.Name]; jobExecuting {
		return
	}

	//构建任务执行状态信息
	jobExecuteInfo = common.BuildJobExecuteInfo(jobPlan)

	//保存执行状态
	scheduler.jobExcutingTable[jobPlan.Job.Name] = jobExecuteInfo

	//执行任务
	fmt.Println("执行任务:", jobExecuteInfo.Job.Name, jobExecuteInfo.PlanTime, jobExecuteInfo.RealTime)
	G_executor.ExecuteJob(jobExecuteInfo)

}

//遍历所有任务
//重新计算任务调度状态,刚进来的时候,如果内存任务表为空,则返回1s时间间隔,否则:
//计算出当前时间,遍历所有任务,判断每一个任务的下次执行时间是否小于当前时间或等于
//当前时间,如果是,说明这个任务该执行了,则执行这个任务,并更新该任务的下次执行时间,
//如果不是,
func (schedule *Scheduler) TrySchedule() (scheduleAfter time.Duration) {
	var (
		jobPlan  *common.JobSchedulerPlan
		now      time.Time
		nearTime *time.Time
	)

	//如果任务表为空的话,睡1秒
	if len(schedule.jobPlanTable) == 0 {
		scheduleAfter = 1 * time.Second
		return
	}

	//任务表有任务
	//当前时间
	now = time.Now()
	//(1)遍历所有任务
	for _, jobPlan = range schedule.jobPlanTable {
		if jobPlan.NextTime.Before(now) || jobPlan.NextTime.Equal(now) {
			//(2)过期任务立即执行
			//任务到期,尝试执行任务(因为如果前一次任务的执行还没有结束,那么这一次就不执行他了)
			fmt.Println("执行任务:", jobPlan.Job.Name)
			schedule.TryStartJob(jobPlan)
			jobPlan.NextTime = jobPlan.Expr.Next(now) //.Next()得到,更新下次执行时间，更新到当前任务上
		}

		//(3)不管任务到期没到期,都要计算出下一个要执行任务距离现在有多久！统计没过期的任务,最近一个要过期的任务时间(N秒后过期 == scheduleAfter)
		if nearTime == nil || jobPlan.NextTime.Before(*nearTime) {
			//nearTime == nil 如何理解？如果nearTime为空指针,则说明，他还没有被赋值,则当前任务的时间就是最近的，则直接赋值
			//第二段判断,如果当前任务的下一次执行时间比刚才最近的时间早,则说明,他更近，则更新一下nearTime
			nearTime = &jobPlan.NextTime
		}
	}

	//下次调度间隔(最近要执行的任务时间-当前时间)=这个时间间隔,之后把这个时间间隔作为咱们上层for循环的时间间隔
	scheduleAfter = (*nearTime).Sub(now)
	return
}

//调度协程,一直读取jobEventChan管道,收到任务变化信息,则维护
//内存中的任务列表
//这里有一个问题,就是我们每次循环读取,一直循环浪费cpu资源,所以我们就已最近一个任务
//要执行的时间间隔，来循环
func (scheduler *Scheduler) schedulerLoop() {
	var (
		jobEvent      *common.JobEvent
		scheduleAfter time.Duration //时间间隔: eg:5s 时间间隔为5s
		scheduleTimer *time.Timer
		jobResult     *common.JobExecuteResult
	)

	//初始化一次(1秒)，有过期任务则执行&&返回下一次要执行的任务距离现在的时间间隔
	//没有任务则返回1s，来作为咱们for循环的间隔
	scheduleAfter = scheduler.TrySchedule()

	//调度的延迟定时器,将时间间隔放进去,eg：5s后,来通知
	scheduleTimer = time.NewTimer(scheduleAfter)

	//定时任务common.Job,监听channel,每当收到一个任务,则判断任务类型,维护到内存表中
	for {
		//select执行：如果没有信号来，则阻塞,有一个信号来，则执行该信号里面的内容
		//select没有满足条件时会被阻塞,一直没有注值就会死锁
		//引入超时机制，
		select {

		case jobEvent = <-scheduler.jobEventChan: //监听任务变化事件,有变化则更新内存任务表
			//对内存中维护的任务列表做增删改查
			scheduler.handleJobEvent(jobEvent)

		case <-scheduleTimer.C: //最近的任务到期了,hold住,等待时间间隔到来了之后，继续往下走

		case jobResult = <-scheduler.jobResultChan: //监听任务执行结果
			//从任务状态内存表中删除对应任务
			scheduler.handleJobResult(jobResult)
		}

		//再循环一次内存任务表&&有过期任务则执行并且返回下一次要执行的任务的时间距离现在的时间间隔为多久,
		//没有过期任务则,返回下一次要执行的任务的时间距离现在的时间间隔为多久
		//调度一次任务
		scheduleAfter = scheduler.TrySchedule()
		//重置调度间隔
		scheduleTimer.Reset(scheduleAfter)
	}

}

//推送任务变化事件
func (scheduler *Scheduler) PushJobEvent(jobEvent *common.JobEvent) {
	scheduler.jobEventChan <- jobEvent
}

//任务回传结果
func (scheduler *Scheduler) PushJobResult(jobResult *common.JobExecuteResult) {
	scheduler.jobResultChan <- jobResult
}

//处理任务结果
func (scheduler *Scheduler) handleJobResult(result *common.JobExecuteResult) {
	var (
		jobLog *common.JobLog
	)
	//删除任务执行状态
	delete(scheduler.jobExcutingTable, result.ExecuteInfo.Job.Name)
	//任务回传结果,将执行结果存在Mongodb中

	//生成执行日志
	if result.Err != common.ERR_LOCK_ALREADY_REQUIRED {
		jobLog = &common.JobLog{
			JobName:      result.ExecuteInfo.Job.Name,
			Command:      result.ExecuteInfo.Job.Command,
			Output:       string(result.Output),
			PlanTime:     result.ExecuteInfo.PlanTime.UnixNano() / 1000 / 1000, //纳秒
			ScheduleTime: result.ExecuteInfo.RealTime.UnixNano() / 1000 / 1000,
			StartTime:    result.StartTime.UnixNano() / 1000 / 1000,
			EndTime:      result.EndTime.UnixNano() / 1000 / 1000,
		}
		if result.Err != nil {
			//有错误,记录
			jobLog.Err = result.Err.Error()
		} else {
			//没有错误
			jobLog.Err = ""
		}
		//存储到Mongodb
		G_logSink.Append(jobLog)

	}
	fmt.Println("任务执行完成:", result.ExecuteInfo.Job.Name, string(result.Output), result.Err)
}

//初始化调度器,不断的循环检查任务是否过期
func InitScheduler() (err error) {
	G_scheduler = &Scheduler{
		jobEventChan:     make(chan *common.JobEvent, 1000),
		jobPlanTable:     make(map[string]*common.JobSchedulerPlan), //内存任务表,map需要初始化的
		jobExcutingTable: make(map[string]*common.JobExecuteInfo),   //任务执行状态表,有任务执行就放入这里面记录,说明他正在执行
		jobResultChan:    make(chan *common.JobExecuteResult, 1000), //任务回传结果
	}
	//启动调度协程
	go G_scheduler.schedulerLoop()
	return
}
